#include "leomedlog/leomedlog.h"

#include <iostream>
#include <vector>
#include <filesystem>
#include <mutex>

// spdlog includes
#include <spdlog/async.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>

// sentry includes
#include <sentry.h>

namespace fs = std::filesystem;

namespace leomedlog {

// Global logger instance
static std::shared_ptr<spdlog::logger> g_logger;

std::shared_ptr<spdlog::logger> GetLogger() {
    return g_logger;
}

// -----------------------------------------------------------------------------
// JSON Formatter
// -----------------------------------------------------------------------------
class JsonFormatter : public spdlog::formatter {
public:
    LogConfig config_;

    explicit JsonFormatter(LogConfig config) : config_(std::move(config)) {}

    void format(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& dest) override {
        nlohmann::json j;

        // 1. Timestamp (ISO 8601)
        // Using spdlog's efficient converter would be better, but for simplicity use fmt
        std::time_t t = std::chrono::system_clock::to_time_t(msg.time);
        // thread-safe gmtime
        std::tm tm_buf;
#ifdef _WIN32
        gmtime_s(&tm_buf, &t);
#else
        gmtime_r(&t, &tm_buf);
#endif
        char time_str[32];
        std::strftime(time_str, sizeof(time_str), "%Y-%m-%dT%H:%M:%S", &tm_buf);
        
        // Add milliseconds
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            msg.time.time_since_epoch()).count() % 1000;
        
        j["ts"] = fmt::format("{}.{:03}Z", time_str, ms);

        // 2. Basic Info
        j["level"] = spdlog::level::to_string_view(msg.level);
        j["logger"] = std::string(msg.logger_name.data(), msg.logger_name.size());
        j["tid"] = msg.thread_id;
        j["app"] = config_.app_name;

        // 3. Message & Payload parsing
        std::string_view payload_view(msg.payload.data(), msg.payload.size());
        static const std::string sep = "|||JSON|||";
        auto pos = payload_view.find(sep);

        if (pos != std::string_view::npos) {
            // Found separator
            j["msg"] = std::string(payload_view.substr(0, pos));
            try {
                auto json_part = payload_view.substr(pos + sep.size());
                j["payload"] = nlohmann::json::parse(json_part);
            } catch (...) {
                j["payload_error"] = "Invalid JSON in payload";
                j["raw_payload"] = std::string(payload_view.substr(pos + sep.size()));
            }
        } else {
            j["msg"] = std::string(payload_view);
        }

        // 4. Source Location (if available)
        if (!msg.source.empty()) {
            j["src"]["file"] = msg.source.filename;
            j["src"]["line"] = msg.source.line;
            j["src"]["func"] = msg.source.funcname;
        }

        // Serialize to buffer
        std::string json_str = j.dump();
        dest.append(json_str.data(), json_str.data() + json_str.size());
        dest.append(std::string_view("\n"));
    }

    std::unique_ptr<spdlog::formatter> clone() const override {
        return std::make_unique<JsonFormatter>(config_);
    }
};

// -----------------------------------------------------------------------------
// Sentry Sink (Breadcrumbs)
// -----------------------------------------------------------------------------
template<typename Mutex>
class SentrySink : public spdlog::sinks::base_sink<Mutex> {
protected:
    void sink_it_(const spdlog::details::log_msg& msg) override {
        // Only convert INFO and above
        if (msg.level < spdlog::level::info) {
            return;
        }

        // Convert level
        sentry_level_t s_level = SENTRY_LEVEL_INFO;
        switch (msg.level) {
            case spdlog::level::warn: s_level = SENTRY_LEVEL_WARNING; break;
            case spdlog::level::err:  s_level = SENTRY_LEVEL_ERROR; break;
            case spdlog::level::critical: s_level = SENTRY_LEVEL_FATAL; break;
            default: break;
        }

        // Parse message/payload similar to JsonFormatter
        // Note: Sentry breadcrumbs store "message" and "data" (map)
        std::string_view payload_view(msg.payload.data(), msg.payload.size());
        static const std::string sep = "|||JSON|||";
        auto pos = payload_view.find(sep);

        std::string message_str;
        nlohmann::json payload_json;
        bool has_payload = false;

        if (pos != std::string_view::npos) {
            message_str = std::string(payload_view.substr(0, pos));
            try {
                payload_json = nlohmann::json::parse(payload_view.substr(pos + sep.size()));
                has_payload = true;
            } catch (...) {}
        } else {
            message_str = std::string(payload_view);
        }

        // Create Breadcrumb
        sentry_value_t crumb = sentry_value_new_breadcrumb("log", message_str.c_str());
        sentry_value_set_by_key(crumb, "level", sentry_value_new_string(
            s_level == SENTRY_LEVEL_INFO ? "info" : 
            s_level == SENTRY_LEVEL_WARNING ? "warning" : 
            s_level == SENTRY_LEVEL_ERROR ? "error" : "fatal"
        ));
        
        // Add payload to data
        if (has_payload && payload_json.is_object()) {
            sentry_value_t data = sentry_value_new_object();
            for (auto& [key, val] : payload_json.items()) {
                if (val.is_string()) {
                    sentry_value_set_by_key(data, key.c_str(), sentry_value_new_string(val.get<std::string>().c_str()));
                } else if (val.is_number_integer()) {
                    sentry_value_set_by_key(data, key.c_str(), sentry_value_new_int32((int32_t)val.get<int64_t>()));
                } else if (val.is_boolean()) {
                     sentry_value_set_by_key(data, key.c_str(), sentry_value_new_bool(val.get<bool>()));
                } else {
                    // Fallback for complex types
                    sentry_value_set_by_key(data, key.c_str(), sentry_value_new_string(val.dump().c_str()));
                }
            }
            sentry_value_set_by_key(crumb, "data", data);
        }

        sentry_add_breadcrumb(crumb);
    }

    void flush_() override {}
};

using SentrySink_mt = SentrySink<std::mutex>;

// -----------------------------------------------------------------------------
// Init / Shutdown
// -----------------------------------------------------------------------------
void Init(const LogConfig& config) {
    try {
        // 1. Setup Paths
        fs::create_directories(config.log_dir);
        fs::create_directories(config.crash_dir);

        // 2. Init Sentry (Crashpad)
        sentry_options_t* options = sentry_options_new();
        sentry_options_set_dsn(options, config.sentry_dsn.c_str());
        sentry_options_set_database_path(options, config.crash_dir.c_str());
        sentry_options_set_release(options, "1.0.0"); // Should pass from config?
        sentry_options_set_environment(options, "production");
        
        // Set handler path explicitly
#ifdef _WIN32
        sentry_options_set_handler_path(options, "crashpad_handler.exe");
#else
        sentry_options_set_handler_path(options, "./crashpad_handler");
#endif

        // Enable crashpad backend
        // Note: Backend is selected at compile time via CMake option usually,
        // but sentry-native ensures the correct one is linked.
        
        if (sentry_init(options) != 0) {
            std::cerr << "[LeoMedLog] Failed to init Sentry!" << std::endl;
            std::abort();
        }

        // 3. Init Spdlog
        spdlog::init_thread_pool(8192, 1); // Queue size 8k, 1 thread
        
        auto rotating_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
            fmt::format("{}/{}.log", config.log_dir, config.app_name), 
            config.max_file_size_mb * 1024 * 1024, 
            config.max_files
        );
        
        // Set JSON Formatter
        rotating_sink->set_formatter(std::make_unique<JsonFormatter>(config));

        auto sentry_sink = std::make_shared<SentrySink_mt>();
        // Sentry sink doesn't need formatting, it parses raw msg

        auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
        // Console sink can use default pattern or json too. Let's use default for debugging.

        std::vector<spdlog::sink_ptr> sinks { rotating_sink, sentry_sink, console_sink };

        g_logger = std::make_shared<spdlog::async_logger>(
            config.app_name, sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block
        );

        // Register global
        spdlog::register_logger(g_logger);
        spdlog::set_default_logger(g_logger);
        
        // Flush on error
        g_logger->flush_on(spdlog::level::err);
        
        LEO_LOG_INFO_KV("LeoMedLog initialized", {"version", "1.0.0"}, {"mode", "Async"});

    } catch (const std::exception& e) {
        std::cerr << "[LeoMedLog] Init Exception: " << e.what() << std::endl;
        std::abort();
    }
}

void Shutdown() {
    LEO_LOG_INFO("LeoMedLog shutting down...");
    spdlog::shutdown(); // Flushes and closes sinks
    sentry_close();
}

} // namespace leomedlog

